# Contents: Song.makehash(path)
# Purpose: Takes one audio file and returns a hash of the audio portion
# Status: All included formats (MP3, Ogg Vorbis, MP4, WMA, FLAC, APE) work.

# required stuff
$:.push("libs") # most of them are in here
require 'digest/md5'
require "mp3info"
require "mp4info"
require "wmainfo"
require "flacinfo"
require 'vorbisfile'
require 'apetag'

class Song

def Song.make_hash(path)
	# find filesize and set default audio boundaries
	fsize = File.stat(path).size
	astart = 0
	aend = fsize

	# different metadata by mime type
	case Song.findType(path)
		# should we also check for id3 tags on non-mp3s because people are weird?
		when "audio/mpeg" # mp3
			(astart, aend) = Song.aBoundsID3(path, fsize)
		when "application/ogg" #ogg
			(astart, aend) = Song.aBoundsOGG(path, fsize)
		when "audio/mp4" # aac
			(astart, aend) = Song.aBoundsMP4(path, fsize)
		when "audio/x-ms-wma" # wma
			(astart, aend) = Song.aBoundsWMA(path, fsize)
		when "audio/x-flac" # flac
			(astart, aend) = Song.aBoundsFLAC(path, fsize)
		when "audio/x-monkeys-audio" # ape
			(astart, aend) = Song.aBoundsAPE(path, fsize)
		else
			puts "Unrecognised type."
	end

	# calculate the hash
	wholehash = nil # declare it here so it works
	File.open(path, 'r') do |fh|
		fh.read(astart)
		wholehash = Digest::MD5.hexdigest(fh.read(aend-astart))
	end

	wholehash # return this
end

private

begin
	require "filemagic" # doesn't work on windows - should throw a LoadError
	def Song.findType(path)
		fm = FileMagic.new(0)
		desc = fm.file(path)
		case desc # parse the output of `file` to find type
			when /MP3/
				return "audio/mpeg"
			when /Ogg/
				return "application/ogg"
			when /MPEG v4/
				return "audio/mp4"
			when /Microsoft ASF/
				return "audio/x-ms-wma" 
			when /FLAC/
				return "audio/x-flac"
			when /Monkey/
				return "audio/x-monkeys-audio"
			else
				return "other"
		end
	end
rescue LoadError
	def Song.findType(path)
		case path # assume the extension is right and parse that
			when /.mp3$/
				return "audio/mpeg"
			when /.ogg$/
				return "application/ogg"
			when /.m4a$/
				return "audio/mp4"
			when /.m4p$/
				return "audio/mp4"
			when /.mp4$/
				return "audio/mp4"
			when /.wma$/
				return "audio/x-ms-wma" 
			when /.flac$/
				return "audio/x-flac"
			when /.ape$/
				return "audio/x-monkeys-audio"
			else
				return "other"
		end
	end
end

def Song.aBoundsID3(path, fsize)
	astart = 0
	aend = fsize
	Mp3Info.open(path) do |meta|
		if (meta.hastag1?()) # ID3v1
			aend = fsize-128
		end
		if (meta.hastag2?()) # ID3v2
			tagsize = 0
			File.open(path) {|f| header = f.read(5)
															foot = (f.getc & 0x1 != 0)
															# This next part reads the 4-byte integer
															4.times {ch = f.getc
																			 tagsize = tagsize << 7 | ch} 
															tagsize += 10
															tagsize += 10 if foot }
			astart = tagsize
		end
	end

	[astart, aend]
end

def Song.aBoundsOGG(path, fsize)
	astart = 0 # so ruby knows about astart
	File.open(path, 'r') do |fh|
		vf = Ogg::VorbisFile.new
		vf.open(fh)
		vf.time_seek(0,0) # seek to the beginning of audio
		astart = vf.raw_tell(0) # the offset in the file
	end

	[astart, fsize]
end

def Song.aBoundsMP4(path, fsize)
	mp4 = MP4Info.open(path)
	astart = fsize - mp4.SIZE

	[astart, fsize] 
end

def Song.aBoundsWMA(path, fsize)
	wma = WmaInfo.new(path)
	astart = wma.headerObject['ASF_Header_Object'][1] # size of header object

	aend = fsize # In reality it's possible there's an index after the data.
	             # In that case, the audio would end before there.  However,
	             # the user is unlikely to mess with that, so we'll include it.

	[astart, aend]
end

def Song.aBoundsFLAC(path, fsize)
	flac = FlacInfo.new(path)

	# get ready to parse prettyprinting of metadata
	rd, wr = IO.pipe # make a pipe
	$stdout = wr # redirect stdout so metadata won't go to screen
	flac.meta_flac # puts metadata in pipe, now we can read it
	$stdout = STDOUT # puts stdout back

	# parse the metadata-data to get its total size
	astart = 0
	last = false # whether this is the last block
	until last
		ln = rd.gets until ln =~ /METADATA/
		rd.gets # discard "type" line
		ln = rd.gets # line with "is last"
		last = (ln =~ /true/ ? true : false) # find out if this is the last block
		ln = rd.gets # line with length
		num = ln =~ /\d/ # find first number in line
		length = ln[num..ln.length-1].to_i # extract length
		astart += length # increment astart
	end
	aend = fsize
	[astart, aend] 
end

def Song.aBoundsAPE(path, fsize)
	ape = ApeTag.new(path)
	astart = 0
	aend = fsize
	if ape.exists?()
		if (ape.tag_start == 0) # tag at beginning
			astart = ape.tag_size + 32
		else # tag at end
			aend = fsize - (ape.tag_size + 32)
		end
	end

	[astart, aend] 
end

end # end 'class'
